/*
 * Copyright (c) 2021 LiuLele
 */


#define _GNU_SOURCE
#include <stddef.h>
#include <stdint.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <endian.h>
#include <errno.h>
#include <scsi/scsi.h>
#include <semaphore.h>

#include "scsi_defs.h"
#include "libtcmu.h"
#include "tcmu-runner.h"
#include "tcmur_device.h"
#include "pfbd/pf_client_api.h"

#define CONCURRENT_VOL_CNT 4
#define MAX_CONCURRENT 16
struct pfbd_vol_stat {
	struct PfClientVolume* vol[MAX_CONCURRENT];
	char *vol_name;
	char *conf_path;
	int concurrent;
};
static void tcmu_pfbd_state_free(struct pfbd_vol_stat *state)
{
	if (state->conf_path)
		free(state->conf_path);
	if (state->vol_name)
		free(state->vol_name);
	free(state);
}
# define MSG_STATUS_REOPEN  0x4000
static void pfbd_io_cbk(void* cbk_arg, int complete_status)
{
	struct tcmur_cmd *cmd = (struct tcmur_cmd *)cbk_arg;
	int rc = 0;
	if(complete_status == MSG_STATUS_REOPEN) {
		tcmu_err("Reopen of PureFlash volume not implemented.\n");
		rc = TCMU_STS_NOT_HANDLED;
	} else if(complete_status != 0){
		tcmu_err("PureFlash IO complete with status:%d.\n", complete_status);
		rc = TCMU_STS_NOT_HANDLED;
	}

	//tcmu_dev_info((struct tcmu_device *)cmd->priv_data, "async complete IO:%p, rc:%d\n", cmd, rc);
	tcmur_cmd_complete((struct tcmu_device *)cmd->priv_data, cmd, rc);
}

static int pfbd_open(struct tcmu_device *dev, bool reopen)
{
	struct pfbd_vol_stat *state;
	char *config, *next_opt, *name;
	int ret;

	state = calloc(1, sizeof(*state));
	if (!state)
		return -ENOMEM;
	state->concurrent = CONCURRENT_VOL_CNT;

	tcmur_dev_set_private(dev, state);
	tcmu_dbg("config %s\n", tcmu_dev_get_cfgstring(dev));
	config = strdup(tcmu_dev_get_cfgstring(dev));
	if (!config) {
		ret = -ENOMEM;
		goto free_state;
	}


	tcmu_dev_info(dev, "Get config str:%s\n", config);
	config = strchr(config, '/');
	if (!config) {
		tcmu_dev_err(dev, "no configuration found in cfgstring\n");
		ret = -EINVAL;
		goto free_config;
	}
	config += 1; /* get past '/' */


	name = strtok(config, ";");
	if (!name) {
		tcmu_dev_err(dev, "Volume name not specified!");
		ret = -EINVAL;
		goto free_config;
	} else {
		tcmu_dev_info(dev, "Volume name:%s\n", name);
		state->vol_name=strdup(name);

	};
	/* The next options are optional */
	next_opt = strtok(NULL, ";");
	while (next_opt) {
		if (!strncmp(next_opt, "conf=", 5)) {
			state->conf_path = strdup(next_opt + 5);
			if (!state->conf_path || !strlen(state->conf_path)) {
				ret = -ENOMEM;
				tcmu_dev_err(dev, "Could not copy conf path.\n");
				goto free_config;
			}
			tcmu_dev_info(dev, "Config file:%s\n", state->conf_path);
		} else if (!strncmp(next_opt, "concurrent=", 11)) {
			state->concurrent = atoi(next_opt + 11);
			if(state->concurrent <= 0) state->concurrent = CONCURRENT_VOL_CNT;
		} else{
			ret = -EINVAL;
			tcmu_dev_err(dev, "Invalid config item:%s\n", next_opt);
			goto free_config;
		}
		next_opt = strtok(NULL, ";");
	}
	tcmu_dev_info(dev, "concurrent number:%d\n", state->concurrent);
	tcmu_dev_set_write_cache_enabled(dev, 0);

	for(int i=0;i<state->concurrent;i++){
		state->vol[i] = pf_open_volume(state->vol_name, state->conf_path, NULL, S5_LIB_VER);;
		if (state->vol[i] == NULL) {
			tcmu_dev_err(dev, "could not open %s: %m\n", state->vol_name);
			goto stop_vol;
		}

	}

	return 0;
stop_vol:

free_config:
	free(config);
free_state:
	tcmu_pfbd_state_free(state);
	return ret;
}

static void pfbd_close(struct tcmu_device *dev)
{
	struct pfbd_vol_stat *state = tcmur_dev_get_private(dev);
	for(int i=0;i<state->concurrent;i++){
		pf_close_volume(state->vol[i]);
	}
	tcmu_pfbd_state_free(state);
}

struct io_waiter
{
	sem_t sem;
	int rc;
};
static void sync_io_cbk(void* cbk_arg, int complete_status)
{
	struct io_waiter* w = (struct io_waiter*)cbk_arg;
	if(complete_status != 0){
		tcmu_err("IO complete in error status:%d\n", complete_status);
	}
	if(w->rc == 0)
		w->rc = complete_status;
	sem_post(&w->sem);
}
static void pfbd_sync_io(struct PfClientVolume* vol, struct io_waiter *waiter, char* buf, size_t length, off_t offset, int is_write)
{
	int ret;

	for(off_t local_off = 0; local_off < length;){
		size_t this_len = (length - local_off) <= PF_MAX_IO_SIZE ? (length - local_off) : PF_MAX_IO_SIZE;
		sem_wait(&waiter->sem);
		if(waiter->rc != 0)
			break;
RESEND:
		ret = pf_io_submit(vol, buf + local_off, this_len, offset + local_off, sync_io_cbk, waiter, is_write);
		if(ret == -EAGAIN){
			usleep(20);
			goto RESEND;
		} else if(ret == -EINVAL) {
			waiter->rc = TCMU_STS_INVALID_CMD;
			sem_post(&waiter->sem);
			break;
		}
		local_off += this_len;
	}
}

static int pfbd_sync_iov(struct PfClientVolume* vol, struct iovec *iov, size_t iov_cnt, size_t length, off_t offset, int is_write)
{
	int iodepth = 8;
	struct io_waiter arg;
	arg.rc=0;
	sem_init(&arg.sem, 0, iodepth);
	for(int i=0;i<iov_cnt;) {
		if(arg.rc != 0)
			break;
		pfbd_sync_io(vol, &arg, iov[i].iov_base, iov[i].iov_len, offset, is_write);
		offset += iov[i].iov_len;
	}
	for(int i=0;i<iodepth;i++){
		sem_wait(&arg.sem);
	}
	if(arg.rc != 0)
		return TCMU_STS_HW_ERR;
	return arg.rc;
}
#define PICK_VOLUME(state, off) (state)->vol[(off>>20)%state->concurrent]
static int pfbd_read(struct tcmu_device *dev, struct tcmur_cmd *cmd,
                     struct iovec *iov, size_t iov_cnt, size_t length,
                     off_t offset)
{
	struct pfbd_vol_stat *state = tcmur_dev_get_private(dev);
	ssize_t ret = TCMU_STS_ASYNC_HANDLED;
	int rc;

	//tcmu_dev_info(dev, "read IO:%p, length:%ld\n", cmd, length);
	cmd->priv_data = dev;
	if(length > PF_MAX_IO_SIZE) {
		ret = pfbd_sync_iov(PICK_VOLUME(state, offset), iov, iov_cnt, length, offset, 0);
		tcmu_dev_info(dev, "sync complete IO:%p, rc:%ld\n", cmd, ret);
		return ret;
	}
RESEND1:
	rc = pf_iov_submit(PICK_VOLUME(state, offset), iov, iov_cnt, length, offset, pfbd_io_cbk, cmd, 0);
	if(rc == -EAGAIN){
		tcmu_dev_warn(dev, "Get EAGAIN for IO:%p, retry ...\n", cmd);
		usleep(20);
		goto RESEND1;
		//ret = TCMU_STS_NO_RESOURCE;
	}
	else if(rc == -EINVAL)
		ret = TCMU_STS_INVALID_CMD;

	return ret;
}


static int pfbd_write(struct tcmu_device *dev, struct tcmur_cmd *cmd,
                      struct iovec *iov, size_t iov_cnt, size_t length,
                      off_t offset)
{
	struct pfbd_vol_stat *state = tcmur_dev_get_private(dev);
	ssize_t ret = TCMU_STS_ASYNC_HANDLED;
	int rc;

	//tcmu_dev_info(dev, "write IO:%p, length:%ld\n", cmd, length);
	cmd->priv_data = dev;
	if(length > PF_MAX_IO_SIZE) {
		ret = pfbd_sync_iov(PICK_VOLUME(state, offset), iov, iov_cnt, length, offset, 1);
		tcmu_dev_info(dev, "sync complete IO:%p, rc:%ld\n", cmd, ret);
		return ret;
	}
RESEND2:
	rc = pf_iov_submit(PICK_VOLUME(state, offset), iov, iov_cnt, length, offset, pfbd_io_cbk, cmd, 1);
	if(rc == -EAGAIN){
		tcmu_dev_warn(dev, "Get EAGAIN for IO:%p, retry ...\n", cmd);
		usleep(20);
		goto RESEND2;
		//ret = TCMU_STS_NO_RESOURCE;
	}
	else if(rc == -EINVAL)
		ret = TCMU_STS_INVALID_CMD;
//done:

	return ret;

}

static int pfbd_flush(struct tcmu_device *dev, struct tcmur_cmd *cmd)
{
	//struct pfbd_vol_stat *state = tcmur_dev_get_private(dev);
	int ret;


	ret = TCMU_STS_OK;
//done:
	return ret;
}

static int pfbd_reconfig(struct tcmu_device *dev, struct tcmulib_cfg_info *cfg)
{
	switch (cfg->type) {
		case TCMULIB_CFG_DEV_SIZE:
			/*
			 * TODO - For open/reconfig we should make sure the FS the
			 * file is on is large enough for the requested size. For
			 * now assume we can grow the file and return 0.
			 */
			return 0;
		case TCMULIB_CFG_DEV_CFGSTR:
		case TCMULIB_CFG_WRITE_CACHE:
		default:
			return -EOPNOTSUPP;
	}
}

static const char pfbd_cfg_desc[] =
		"Name of pfbd volume to use as a backstore.";

static struct tcmur_handler pfbd_handler = {
		.cfg_desc = pfbd_cfg_desc,

		.reconfig = pfbd_reconfig,

		.open = pfbd_open,
		.close = pfbd_close,
		.read = pfbd_read,
		.write = pfbd_write,
		.flush = pfbd_flush,
		.name = "Pfbd-backed Handler",
		.subtype = "pfbd",
		.nr_threads = 2,
};

/* Entry point must be named "handler_init". */
int handler_init(void)
{
	return tcmur_register_handler(&pfbd_handler);
}
